package com.yu.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.Week;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yu.shortlink.project.common.convention.exception.ClientException;
import com.yu.shortlink.project.common.convention.exception.ServiceException;
import com.yu.shortlink.project.common.enums.VailDateTypeEnum;
import com.yu.shortlink.project.dao.entity.*;
import com.yu.shortlink.project.dao.mapper.*;
import com.yu.shortlink.project.dto.req.ShortLinkAccessRecordReqDTO;
import com.yu.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.yu.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.yu.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.yu.shortlink.project.dto.resp.ShortLinkCreateRespDTO;
import com.yu.shortlink.project.dto.resp.ShortLinkGroupCountQueryRespDTO;
import com.yu.shortlink.project.dto.resp.ShortLinkPageRespDTO;
import com.yu.shortlink.project.dto.resp.ShortLinkStatsAccessRecordRespDTO;
import com.yu.shortlink.project.service.ShortLinkGotoDOService;
import com.yu.shortlink.project.service.ShortLinkService;
import com.yu.shortlink.project.toolkit.HashUtil;
import com.yu.shortlink.project.toolkit.LinkUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import static com.yu.shortlink.project.common.constant.RedisKeyConstant.*;
import static com.yu.shortlink.project.common.constant.ShortLinkConstant.AMAP_REMOTE_URL;

/**
 * @author 哈哈哈呜呜呜
 * @description 针对表【t_link】的数据库操作Service实现
 * @createDate 2024-04-23 09:47:04
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO>
        implements ShortLinkService {

    private final ShortLinkGotoDOService shortLinkGotoDOService;

    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;

    private final StringRedisTemplate stringRedisTemplate;

    private final RedissonClient redissonClient;

    private final LinkAccessStatsDOMapper linkAccessStatsDOMapper;

    private final LinkLocaleStatsDOMapper linkLocaleStatsDOMapper;

    private final LinkOsStatsDoMapper linkOsStatsDoMapper;

    private final LinkBrowserStatsDoMapper linkBrowserStatsDoMapper;

    private final LinkAccessLogsDOMapper linkAccessLogsDOMapper;

    private final LinkDeviceStatsMapper linkDeviceStatsMapper;

    private final LinkNetworkStatsMapper linkNetworkStatsMapper;

    @Value("${short-link.stats.locale.amap-key}")
    private String statsLocaleAmapKey;

    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
        String shortLinkSuffix = generateSuffix(requestParam);
        String fullShortUrl = requestParam.getDomain() + "/" + shortLinkSuffix;
        ShortLinkDO shortLinkDO = BeanUtil.toBean(requestParam, ShortLinkDO.class);
        shortLinkDO.setFullShortUrl(fullShortUrl);
        shortLinkDO.setShortUri(shortLinkSuffix);
        shortLinkDO.setEnableStatus(0);
        shortLinkDO.setFavicon(getFavicon(requestParam.getOriginUrl()));
        shortLinkDO.setTotalPv(0);
        shortLinkDO.setTotalUip(0);
        shortLinkDO.setTotalUv(0);

        ShortLinkGotoDO shortLinkGotoDO = ShortLinkGotoDO.builder().fullShortUrl(fullShortUrl).gid(requestParam.getGid()).build();

        try {
            baseMapper.insert(shortLinkDO);
            shortLinkGotoDOService.save(shortLinkGotoDO);
        } catch (DuplicateKeyException e) {
            log.warn("短链接：{}重复入库", fullShortUrl);
            throw new ServiceException("短链接生成重复");
        }
        stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl),
                requestParam.getOriginUrl(),
                LinkUtil.getLinkCacheValidDate(requestParam.getVaildDate()), TimeUnit.MILLISECONDS);
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl(shortLinkDO.getFullShortUrl())
                .gid(requestParam.getGid())
                .originUrl(requestParam.getOriginUrl())
                .build();
    }



    @Override
    @Transactional
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        // 比对新旧gid
        if (Objects.equals(requestParam.getOldGid(), requestParam.getNewGid())) {
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, requestParam.getOldGid())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .set(Objects.equals(requestParam.getVaildDateType(), VailDateTypeEnum.PERMANENT.getType()), ShortLinkDO::getVaildDate, null);
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .gid(requestParam.getOldGid())
                    .originUrl(requestParam.getOriginUrl())
                    .describe(requestParam.getDescribe())
                    .vaildDateType(requestParam.getVaildDateType())
                    .vaildDate(requestParam.getVaildDate())
                    .favicon(requestParam.getFavicon())
                    .build();
            baseMapper.update(shortLinkDO, updateWrapper);
            return;
        }
        // 删除原始gid数据
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(ShortLinkDO::getGid, requestParam.getOldGid())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        try {
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
            if (Objects.isNull(shortLinkDO)) {
                throw new ClientException("短链接不存在");
            }
            // 删除原始短链接 新增新的短链接 因为link表的分片时gid
            baseMapper.deleteById(shortLinkDO);
            ShortLinkDO saveShortLinkDO = ShortLinkDO.builder()
                    .gid(requestParam.getNewGid())
                    .originUrl(requestParam.getOriginUrl())
                    .describe(requestParam.getDescribe())
                    .vaildDateType(requestParam.getVaildDateType())
                    .vaildDate(requestParam.getVaildDate())
                    .favicon(requestParam.getFavicon())
                    .build();
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.copyProperties(saveShortLinkDO, ShortLinkCreateReqDTO.class);
            shortLinkCreateReqDTO.setGid(requestParam.getNewGid());
            this.createShortLink(shortLinkCreateReqDTO);
        } catch (Exception e) {
            throw new ClientException("短链接有多个，数据异常");
        }

    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getGid())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0)
                .orderByDesc(ShortLinkDO::getCreateTime);
        IPage<ShortLinkDO> resultPage = baseMapper.selectPage(requestParam, queryWrapper);
        return resultPage.convert(each -> BeanUtil.toBean(each, ShortLinkPageRespDTO.class));
    }

    @Override
    public List<ShortLinkGroupCountQueryRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(ShortLinkDO.builder().build())
                .select("gid", "count(*) as shortLinkCount")
                .in("gid", requestParam)
                .eq("enable_status", 0)
                .groupBy("gid");
        List<Map<String, Object>> shortLinkDOList = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(shortLinkDOList, ShortLinkGroupCountQueryRespDTO.class);
    }

    @SneakyThrows
    @Override
    public void restoreUrl(String shortUri, HttpServletRequest request, HttpServletResponse response) {
        String serverName = request.getServerName();
        String fullShortUrl = serverName + "/" + shortUri;
        String originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(originalLink)) {
            response.sendRedirect(originalLink);
            shortLinkStats(fullShortUrl,null, request,response);
        } else {
            boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);
            if (!contains) {
                response.sendRedirect("/page/notfound");
                return;
            }
            String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
                response.sendRedirect("/page/notfound");
                return;
            }
            RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
            lock.lock();
            try {
                originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
                shortLinkStats(fullShortUrl,null, request,response);
                if (StrUtil.isBlank(originalLink)) {
                    // 查询goto表
                    ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoDOService.getOne(Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                            .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl));
                    if (shortLinkGotoDO == null) {
                        response.sendRedirect("/page/notfound");
                        stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                        return;
                    }
                    LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                            .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                            .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                            .eq(ShortLinkDO::getDelFlag, 0)
                            .eq(ShortLinkDO::getEnableStatus, 0);
                    ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
                    if (shortLinkDO == null || (shortLinkDO.getVaildDate() != null && shortLinkDO.getVaildDate().before(new Date()))) {
                            stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-",30,TimeUnit.MILLISECONDS);
                            response.sendRedirect("/page/notfound");
                    }
                    shortLinkStats(fullShortUrl, shortLinkDO.getGid(), request,response);
                }
            } finally {
                lock.unlock();
            }
        }
    }

    @Override
    public IPage<ShortLinkStatsAccessRecordRespDTO> shortLinkAccessRecord(ShortLinkAccessRecordReqDTO requestParam) {
        LambdaQueryWrapper<LinkAccessLogsDO> wrapper = Wrappers.<LinkAccessLogsDO>lambdaQuery()
                .eq(LinkAccessLogsDO::getGid, requestParam.getGid())
                .eq(LinkAccessLogsDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(LinkAccessLogsDO::getDelFlag, 0)
                .between(LinkAccessLogsDO::getCreateTime,requestParam.getStartDate(),requestParam.getEndDate())
                .orderByDesc(LinkAccessLogsDO::getCreateTime);
        IPage<LinkAccessLogsDO> linkAccessLogsPage = linkAccessLogsDOMapper.selectPage(requestParam, wrapper);
        IPage<ShortLinkStatsAccessRecordRespDTO> shortLinkStatsAccessRecordDTOIPage = linkAccessLogsPage.convert(e -> BeanUtil.toBean(e, ShortLinkStatsAccessRecordRespDTO.class));

        if (CollUtil.isNotEmpty(linkAccessLogsPage.getRecords())){
            List<String> userAccessLogsList = shortLinkStatsAccessRecordDTOIPage.getRecords().stream().map(ShortLinkStatsAccessRecordRespDTO::getUser).collect(Collectors.toList());
            List<Map<String,Object>> uvTypeList  = linkAccessLogsDOMapper.selectUvTypeByUsers(requestParam,userAccessLogsList);
            shortLinkStatsAccessRecordDTOIPage.getRecords().stream().map(each->{
                uvTypeList.stream()
                        .filter(item -> Objects.equals(each.getUser(), item.get("user")))
                        .findFirst().
                        ifPresent(e->each.setUvType((String) e.get("uvType")));
                return each;
            }).collect(Collectors.toList());
        }
        return shortLinkStatsAccessRecordDTOIPage;
    }


    /*********************************************************************************/
    private void shortLinkStats(String fullShortUrl, String gid, HttpServletRequest request, HttpServletResponse response){
        AtomicReference<String> uv = new AtomicReference<>();
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        Cookie[] cookies = request.getCookies();
        try {
            Runnable addResponseCookieTask = () ->{
                uv.set(UUID.randomUUID().toString());
                Cookie uvCookie = new Cookie("uv", uv.get());
                uvCookie.setMaxAge(60 * 60 * 24 * 30);
                uvCookie.setPath(StrUtil.sub(fullShortUrl,fullShortUrl.indexOf("/"),fullShortUrl.length()));
                response.addCookie(uvCookie);
                uvFirstFlag.set(Boolean.TRUE);
                stringRedisTemplate.opsForSet().add("short-link:stats:uv"+ fullShortUrl,uv.get());
            };
            if (ArrayUtil.isNotEmpty(cookies)){
                Arrays.stream(cookies)
                        .filter(each -> Objects.equals(each.getName(),"uv"))
                        .findFirst()
                        .map(Cookie::getValue)
                        .ifPresentOrElse(each ->{
                            Long uvAdded =stringRedisTemplate.opsForSet().add("short-link:stats:uv"+ fullShortUrl,each);
                            uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
                        },addResponseCookieTask);
            }

            String remoteAddr = request.getRemoteAddr();
            Long uipAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uip" + fullShortUrl, remoteAddr);
            boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;

            if (StrUtil.isNotBlank(gid)){
                ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoDOService.getOne(Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl));
                gid = shortLinkGotoDO.getGid();
            }
            int hour = DateUtil.hour(new Date(),true);
            Week week = DateUtil.dayOfWeekEnum(new Date());
            int weekValue = week.getIso8601Value();
            LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                    .pv(1)
                    .uv(uvFirstFlag.get() ? 1 : 0)
                    .uip(uipFirstFlag ? 1 : 0)
                    .hour(hour)
                    .weekday(weekValue)
                    .fullShortUrl(fullShortUrl)
                    .gid(gid)
                    .date(new Date())
                    .build();
            linkAccessStatsDOMapper.shortLinkStats(linkAccessStatsDO);



            HashMap<String, Object> localeParamMap = new HashMap<>();
            localeParamMap.put("key",statsLocaleAmapKey);
            localeParamMap.put("ip",remoteAddr);
            String localResultStr = HttpUtil.get(AMAP_REMOTE_URL, localeParamMap);
            JSONObject localResultObj = JSON.parseObject(localResultStr);
            String infocode = localResultObj.getString("infocode");
            if (StrUtil.isNotBlank(infocode) && StrUtil.equals(infocode,"1000")){
                String actualProvince,
                        actualCity;

                String province = localResultObj.getString("province");
                boolean unknownFlag = StrUtil.isBlank(province);
                actualProvince = unknownFlag ? "未知" : province;
                actualCity = unknownFlag ? "未知" : localResultObj.getString("city");
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .fullShortUrl(fullShortUrl)
                        .gid(gid)
                        .province(actualProvince)
                        .city(actualCity)
                        .adcode(unknownFlag ? "未知" : localResultObj.getString("adcode"))
                        .country("中国")
                        .cnt(1)
                        .date(new Date())
                        .build();
                linkLocaleStatsDOMapper.shortLinkLocaleStats(linkLocaleStatsDO);


                LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                        .os(LinkUtil.getOs(request))
                        .cnt(1)
                        .date(new Date())
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .build();
                linkOsStatsDoMapper.shortLinkOsStats(linkOsStatsDO);


                LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                        .browser(LinkUtil.getBrowser(request))
                        .cnt(1)
                        .date(new Date())
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .build();
                linkBrowserStatsDoMapper.shortLinkOsStats(linkBrowserStatsDO);

                String device = LinkUtil.getDevice(request);
                LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                        .device(LinkUtil.getDevice(request))
                        .cnt(1)
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .build();
                linkDeviceStatsMapper.shortLinkDeviceState(linkDeviceStatsDO);

                String network = LinkUtil.getNetwork(request);
                LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                        .network(network)
                        .cnt(1)
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .build();
                linkNetworkStatsMapper.shortLinkNetworkState(linkNetworkStatsDO);

                LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                        .user(uv.get())
                        .ip(remoteAddr)
                        .browser(LinkUtil.getBrowser(request))
                        .os(LinkUtil.getOs(request))
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .network(network)
                        .device(device)
                        .locale(StrUtil.join("-","中国",actualProvince,actualCity))
                        .build();
                linkAccessLogsDOMapper.insert(linkAccessLogsDO);

                baseMapper.incrementStats(gid,fullShortUrl,1,uvFirstFlag.get() ? 1 : 0,uipFirstFlag ? 1 : 0);
            }

        } catch (Exception e) {
            log.error("短链接访问量统计异常",e);
        }
    }

    private String generateSuffix(ShortLinkCreateReqDTO requestParam) {
        int customGenerateCount = 0;
        String shorUri;
        while (true) {
            if (customGenerateCount > 10) {
                throw new ServiceException("短链接频繁生成，请稍后再试");
            }
            String originUrl = requestParam.getOriginUrl();
            originUrl += UUID.randomUUID().toString();
            shorUri = HashUtil.hashToBase62(originUrl);
            if (!shortUriCreateCachePenetrationBloomFilter.contains(requestParam.getDomain() + "/" + shorUri)) {
                break;
            }
            customGenerateCount++;
        }
        return shorUri;
    }


    @SneakyThrows
    private String getFavicon(String url) {
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_OK){
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null){
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }
}




